package stashpullrequestbuilder.stashpullrequestbuilder.stash;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ObjectNode;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Created by Nathan McCarthy
*/
@SuppressFBWarnings("EQ_DOESNT_OVERRIDE_EQUALS")
public class StashApiClient {
private static final int HTTP_REQUEST_TIMEOUT_SECONDS = 60;
private static final int HTTP_CONNECTION_TIMEOUT_SECONDS = 15;
private static final int HTTP_SOCKET_TIMEOUT_SECONDS = 15;
private static final Logger logger = Logger.getLogger(StashApiClient.class.getName());
private static final ObjectMapper mapper = new ObjectMapper();
private String apiBaseUrl;
private String project;
private String repositoryName;
private Credentials credentials;
private boolean ignoreSsl;
public StashApiClient(String stashHost, String username, String password, String project, String repositoryName, boolean ignoreSsl) {
this.credentials = new UsernamePasswordCredentials(username, password);
this.project = project;
this.repositoryName = repositoryName;
this.apiBaseUrl = stashHost.replaceAll("/$", "") + "/rest/api/1.0/projects/";
this.ignoreSsl = ignoreSsl;
}
public List<StashPullRequestResponseValue> getPullRequests() {
List<StashPullRequestResponseValue> pullRequestResponseValues = new ArrayList<StashPullRequestResponseValue>();
try {
boolean isLastPage = false;
int start = 0;
while (!isLastPage) {
String response = getRequest(pullRequestsPath(start));
StashPullRequestResponse parsedResponse = parsePullRequestJson(response);
isLastPage = parsedResponse.getIsLastPage();
if (!isLastPage) {
start = parsedResponse.getNextPageStart();
}
pullRequestResponseValues.addAll(parsedResponse.getPrValues());
}
return pullRequestResponseValues;
} catch (IOException e) {
logger.log(Level.WARNING, "invalid pull request response.", e);
}
return Collections.EMPTY_LIST;
}
public List<StashPullRequestComment> getPullRequestComments(String projectCode, String commentRepositoryName,
String pullRequestId) {
try {
boolean isLastPage = false;
int start = 0;
List<StashPullRequestActivityResponse> commentResponses = new ArrayList<StashPullRequestActivityResponse>();
while (!isLastPage) {
String response = getRequest(
apiBaseUrl + projectCode + "/repos/" + commentRepositoryName + "/pull-requests/" +
pullRequestId + "/activities?start=" + start);
StashPullRequestActivityResponse resp = parseCommentJson(response);
isLastPage = resp.getIsLastPage();
if (!isLastPage) {
start = resp.getNextPageStart();
}
commentResponses.add(resp);
}
return extractComments(commentResponses);
} catch (Exception e) {
logger.log(Level.WARNING, "invalid pull request response.", e);
}
return Collections.EMPTY_LIST;
}
public void deletePullRequestComment(String pullRequestId, String commentId) {
String path = pullRequestPath(pullRequestId) + "/comments/" + commentId + "?version=0";
deleteRequest(path);
}
public StashPullRequestComment postPullRequestComment(String pullRequestId, String comment) {
String path = pullRequestPath(pullRequestId) + "/comments";
try {
String response = postRequest(path, comment);
return parseSingleCommentJson(response);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to post Stash PR comment " + path + " " + e);
}
return null;
}
public StashPullRequestMergableResponse getPullRequestMergeStatus(String pullRequestId) {
String path = pullRequestPath(pullRequestId) + "/merge";
try {
String response = getRequest(path);
return parsePullRequestMergeStatus(response);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to get Stash PR Merge Status " + path + " " + e);
}
return null;
}
public boolean mergePullRequest(String pullRequestId, String version) {
String path = pullRequestPath(pullRequestId) + "/merge?version=" + version;
try {
String response = postRequest(path, null);
return !response.equals(Integer.toString(HttpStatus.SC_CONFLICT));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to merge Stash PR " + path + " " + e);
}
return false;
}
private HttpContext gethttpContext(Credentials credentials) {
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(AuthScope.ANY, credentials);
AuthCache authCache = new BasicAuthCache();
BasicScheme basicAuth = new BasicScheme();
URI stashUri = URI.create(this.apiBaseUrl);
authCache.put(new HttpHost(stashUri.getHost(), stashUri.getPort(), stashUri.getScheme()), basicAuth);
HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(credsProvider);
context.setAuthCache(authCache);
RequestConfig config = RequestConfig.copy(context.getRequestConfig()).
setConnectTimeout(StashApiClient.HTTP_CONNECTION_TIMEOUT_SECONDS * 1000).
setSocketTimeout(StashApiClient.HTTP_SOCKET_TIMEOUT_SECONDS * 1000).build();
context.setRequestConfig(config);
return context;
}
private HttpClient getHttpClient() {
HttpClientBuilder builder = HttpClientBuilder.create().useSystemProperties();
if (this.ignoreSsl) {
try {
SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
sslContextBuilder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContextBuilder.build(), NoopHostnameVerifier.INSTANCE);
builder.setSSLSocketFactory(sslsf);
} catch (NoSuchAlgorithmException e) {
logger.log(Level.SEVERE, "Failing to setup the SSLConnectionFactory: " + e.toString());
throw new RuntimeException(e);
} catch (KeyStoreException e) {
logger.log(Level.SEVERE, "Failing to setup the SSLConnectionFactory: " + e.toString());
throw new RuntimeException(e);
} catch (KeyManagementException e) {
logger.log(Level.SEVERE, "Failing to setup the SSLConnectionFactory: " + e.toString());
throw new RuntimeException(e);
}
}
return builder.build();
}
private String getRequest(String path) {
logger.log(Level.FINEST, "PR-GET-REQUEST:" + path);
HttpClient client = getHttpClient();
HttpContext context = gethttpContext(credentials);
HttpGet httpget = new HttpGet(path);
//http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html; section 14.10.
//tells the server that we want it to close the connection when it has sent the response.
//address large amount of close_wait sockets client and fin sockets server side
httpget.addHeader("Connection", "close");
String response = null;
FutureTask<String> httpTask = null;
Thread thread;
try {
//Run the http request in a future task so we have the opportunity
//to cancel it if it gets hung up; which is possible if stuck at
//socket native layer. see issue JENKINS-30558
httpTask = new FutureTask<String>(new Callable<String>() {
private HttpClient client;
private HttpContext context;
private HttpGet httpget;
@Override
public String call() throws Exception {
HttpResponse httpResponse = client.execute(httpget, context);
int responseCode = httpResponse.getStatusLine().getStatusCode();
String response = httpResponse.getStatusLine().getReasonPhrase();
if (!validResponseCode(responseCode)) {
logger.log(Level.SEVERE, "Failing to get response from Stash PR GET" + httpget.getURI().getPath());
throw new RuntimeException("Didn't get a 200 response from Stash PR GET! Response; '" +
responseCode + "' with message; " + response);
}
InputStream responseBodyAsStream = httpResponse.getEntity().getContent();
StringWriter stringWriter = new StringWriter();
IOUtils.copy(responseBodyAsStream, stringWriter, "UTF-8");
response = stringWriter.toString();
return response;
}
public Callable<String> init(HttpClient client, HttpGet httpget, HttpContext context) {
this.client = client;
this.context = context;
this.httpget = httpget;
return this;
}
}.init(client, httpget, context));
thread = new Thread(httpTask);
thread.start();
response = httpTask.get((long) StashApiClient.HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (TimeoutException e) {
e.printStackTrace();
httpget.abort();
throw new RuntimeException(e);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
httpget.releaseConnection();
}
logger.log(Level.FINEST, "PR-GET-RESPONSE:" + response);
return response;
}
public void deleteRequest(String path) {
HttpClient client = getHttpClient();
HttpContext context = gethttpContext(this.credentials);
HttpDelete httpDelete = new HttpDelete(path);
//http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html; section 14.10.
//tells the server that we want it to close the connection when it has sent the response.
//address large amount of close_wait sockets client and fin sockets server side
httpDelete.setHeader("Connection", "close");
int res = -1;
FutureTask<Integer> httpTask = null;
Thread thread;
try {
//Run the http request in a future task so we have the opportunity
//to cancel it if it gets hung up; which is possible if stuck at
//socket native layer. see issue JENKINS-30558
httpTask = new FutureTask<Integer>(new Callable<Integer>() {
private HttpClient client;
private HttpContext context;
private HttpDelete httpDelete;
@Override
public Integer call() throws Exception {
int res = -1;
res = client.execute(httpDelete, context).getStatusLine().getStatusCode();
return res;
}
public Callable<Integer> init(HttpClient client, HttpDelete httpDelete, HttpContext context) {
this.client = client;
this.httpDelete = httpDelete;
this.context = context;
return this;
}
}.init(client, httpDelete, context));
thread = new Thread(httpTask);
thread.start();
res = httpTask.get((long) StashApiClient.HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (TimeoutException e) {
e.printStackTrace();
httpDelete.abort();
throw new RuntimeException(e);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
httpDelete.releaseConnection();
}
logger.log(Level.FINE, "Delete comment {" + path + "} returned result code; " + res);
}
private String postRequest(String path, String comment) throws UnsupportedEncodingException {
logger.log(Level.FINEST, "PR-POST-REQUEST:" + path + " with: " + comment);
HttpClient client = getHttpClient();
HttpContext context = gethttpContext(credentials);
HttpPost httppost = new HttpPost(path);
//http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html; section 14.10.
//tells the server that we want it to close the connection when it has sent the response.
//address large amount of close_wait sockets client and fin sockets server side
httppost.setHeader("Connection", "close");
httppost.setHeader("X-Atlassian-Token", "no-check"); //xsrf
if (comment != null) {
ObjectNode node = mapper.getNodeFactory().objectNode();
node.put("text", comment);
StringEntity requestEntity = null;
try {
requestEntity = new StringEntity(
mapper.writeValueAsString(node),
ContentType.APPLICATION_JSON);
} catch (IOException e) {
e.printStackTrace();
}
httppost.setEntity(requestEntity);
}
String response = "";
FutureTask<String> httpTask = null;
Thread thread;
try {
//Run the http request in a future task so we have the opportunity
//to cancel it if it gets hung up; which is possible if stuck at
//socket native layer. see issue JENKINS-30558
httpTask = new FutureTask<String>(new Callable<String>() {
private HttpClient client;
private HttpContext context;
private HttpPost httppost;
@Override
public String call() throws Exception {
HttpResponse httpResponse = client.execute(httppost, context);
int responseCode = httpResponse.getStatusLine().getStatusCode();
String response = httpResponse.getStatusLine().getReasonPhrase();
if (!validResponseCode(responseCode)) {
logger.log(Level.SEVERE, "Failing to get response from Stash PR POST" + httppost.getURI().getPath());
throw new RuntimeException("Didn't get a 200 response from Stash PR POST! Response; '" +
responseCode + "' with message; " + response);
}
InputStream responseBodyAsStream = httpResponse.getEntity().getContent();
StringWriter stringWriter = new StringWriter();
IOUtils.copy(responseBodyAsStream, stringWriter, "UTF-8");
response = stringWriter.toString();
logger.log(Level.FINEST, "API Request Response: " + response);
return response;
}
public Callable<String> init(HttpClient client, HttpPost httppost, HttpContext context) {
this.client = client;
this.context = context;
this.httppost = httppost;
return this;
}
}.init(client, httppost, context));
thread = new Thread(httpTask);
thread.start();
response = httpTask.get((long) StashApiClient.HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (TimeoutException e) {
e.printStackTrace();
httppost.abort();
throw new RuntimeException(e);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
} finally {
httppost.releaseConnection();
}
logger.log(Level.FINEST, "PR-POST-RESPONSE:" + response);
return response;
}
private boolean validResponseCode(int responseCode) {
return responseCode == HttpStatus.SC_OK ||
responseCode == HttpStatus.SC_ACCEPTED ||
responseCode == HttpStatus.SC_CREATED ||
responseCode == HttpStatus.SC_NO_CONTENT ||
responseCode == HttpStatus.SC_RESET_CONTENT;
}
private StashPullRequestResponse parsePullRequestJson(String response) throws IOException {
StashPullRequestResponse parsedResponse;
parsedResponse = mapper.readValue(response, StashPullRequestResponse.class);
return parsedResponse;
}
private StashPullRequestActivityResponse parseCommentJson(String response) throws IOException {
StashPullRequestActivityResponse parsedResponse;
parsedResponse = mapper.readValue(response, StashPullRequestActivityResponse.class);
return parsedResponse;
}
private List<StashPullRequestComment> extractComments(List<StashPullRequestActivityResponse> responses) {
List<StashPullRequestComment> comments = new ArrayList<StashPullRequestComment>();
for (StashPullRequestActivityResponse parsedResponse : responses) {
for (StashPullRequestActivity a : parsedResponse.getPrValues()) {
if (a != null && a.getComment() != null) comments.add(a.getComment());
}
}
return comments;
}
private StashPullRequestComment parseSingleCommentJson(String response) throws IOException {
StashPullRequestComment parsedResponse;
parsedResponse = mapper.readValue(
response,
StashPullRequestComment.class);
return parsedResponse;
}
protected static StashPullRequestMergableResponse parsePullRequestMergeStatus(String response) throws IOException {
StashPullRequestMergableResponse parsedResponse;
parsedResponse = mapper.readValue(
response,
StashPullRequestMergableResponse.class);
return parsedResponse;
}
private String pullRequestsPath() {
return apiBaseUrl + this.project + "/repos/" + this.repositoryName + "/pull-requests/";
}
private String pullRequestPath(String pullRequestId) {
return pullRequestsPath() + pullRequestId;
}
private String pullRequestsPath(int start) {
String basePath = pullRequestsPath();
return basePath.substring(0, basePath.length() - 1) + "?start=" + start;
}
}